/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2009 - 2012 Luca Mingardi.
 *
 * This file is part of jeeObserver.
 *
 * JeeObserver is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * JeeObserver is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
package jeeobserver.server;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import jeeobserver.HttpSessionObserver;
import jeeobserver.server.DatabaseFileElements.DatabaseElementEntity;
import jeeobserver.server.DatabaseFileStatistics.DatabaseStatisticsEntity;
import jeeobserver.server.HttpSessionParameters.DeleteStatisticsParameter;
import jeeobserver.server.HttpSessionParameters.SearchElementsParameter;
import jeeobserver.server.HttpSessionParameters.SearchStatisticsParameter;
import jeeobserver.server.HttpSessionStatistics.Element;
import jeeobserver.server.HttpSessionStatistics.TimeStatistics;
import jeeobserver.server.HttpSessionStatistics.TotalStatistics;
import jeeobserver.server.Statistics.DateStatistics;
import jeeobserver.server.Statistics.NumberStatistics;
import jeeobserver.utilities.DatabaseUtilities;

class FileDatabaseHandlerHttpSession {

	 private static final String ELEMENTS_FILE_NAME = "http_session_elements";

	 private static final String STATISTICS_FILE_NAME = "http_session_statistics";

	 private String path;

	 private int samplingPeriod;

	 private DatabaseFileElements<ElementEntity> fileElements;

	 private DatabaseFileStatistics fileStatistics;

	 //Chached indexes
	 FileDatabaseHandlerHttpSession(String path, int samplingPeriod) throws IOException {

			this.path = path;

			this.samplingPeriod = samplingPeriod;

			//Create files (if not exists)
			this.fileElements = new DatabaseFileElements(this.path + ELEMENTS_FILE_NAME, ElementEntity.class);

			this.fileStatistics = new DatabaseFileStatistics(this.path + STATISTICS_FILE_NAME, StatisticsEntity.class, StatisticsEntity.BYTES_SIZE, this.samplingPeriod);
	 }

	 synchronized void persistStatistics(Collection<HttpSessionObserver> observers) throws IOException {

			if (observers.isEmpty()) {
				 return;
			}

			JeeObserverServerContext.logger.log(Level.FINER, "Persisting {0} Http Session statistics.", String.valueOf(observers.size()));

			Map<Integer, DatabaseStatisticsEntity> statisticsMap = new HashMap<Integer, DatabaseStatisticsEntity>();

			Map<Integer, DatabaseElementEntity> elementsMap = new HashMap<Integer, DatabaseElementEntity>();


			//Iterate and group all invocations
			for (final HttpSessionObserver observer : observers) {

				 //Find statistics sampling date
				 final Date samplingDate = DatabaseUtilities.calculateStatisticTimestamp(observer.getDate(), this.samplingPeriod);

				 //Insert into cache all elements
				 ElementEntity elementEntity = new ElementEntity(observer.getContext(), observer.getProject());

				 if (!elementsMap.containsKey(elementEntity.getElement().getId())) {
						elementsMap.put(elementEntity.getElement().getId(), elementEntity);
				 }

				 //Insert into cache all statistics
				 StatisticsEntity statisticsEntity = new StatisticsEntity(elementEntity.getElement().getId(), samplingDate, observer);

				 int statisticsId = (statisticsEntity.getElementId() + "|" + statisticsEntity.getDate().getTime()).hashCode();

				 if (!statisticsMap.containsKey(statisticsId)) {
						statisticsMap.put(statisticsId, statisticsEntity);
				 } else {
						statisticsMap.get(statisticsId).add(statisticsEntity);
				 }
			}

			//Persist cached elements into Database
			this.fileElements.persist(elementsMap.values());

			//Persist cached statistics into Database
			this.fileStatistics.persist(statisticsMap.values());
	 }

	 synchronized Collection<Element> searchElements(SearchElementsParameter parameters) throws IOException {

			JeeObserverServerContext.logger.log(Level.FINE, "Searching Http Session elements. {0}", parameters.toString());

			final Collection<Element> resultList = new ArrayList<Element>();

			this.fileElements.reset();

			ElementEntity elementEntity;

			while ((elementEntity = (ElementEntity) this.fileElements.read()) != null) {
				 if (elementEntity.passFilter(parameters.getElementId(), parameters.getContext(), parameters.getProject())) {
						resultList.add(elementEntity.getElement());
				 }
			}

			return resultList;
	 }

	 synchronized Collection<HttpSessionStatistics> searchStatistics(SearchStatisticsParameter parameters) throws IOException {

			JeeObserverServerContext.logger.log(Level.FINE, "Searching Http Session statistics. {0}", parameters.toString());

			//Check groupBy parameter
			if (parameters.getGroupBy() != RequestParameter.GROUP_BY_CONTEXT && parameters.getGroupBy() != RequestParameter.GROUP_BY_CONTEXT_PROJECT) {
				 throw new IOException("GroupBy: " + parameters.getGroupBy() + " parameter not valid.");
			}

			//Search elements
			Collection<Element> elements = this.searchElements(new SearchElementsParameter(parameters.getElementId(), parameters.getContext(), parameters.getProject()));

			//Group by
			Map<Integer, Element> elementsGroupedElements = new HashMap<Integer, Element>();
			Map<Integer, Integer> elementsGroupedIds = new HashMap<Integer, Integer>();

			for (Element element : elements) {

				 int elementGroupedId = 0;

				 if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT) {
						elementGroupedId = (element.getContext()).hashCode();
				 } else if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT_PROJECT) {
						elementGroupedId = element.getId();
				 }

				 if (!elementsGroupedIds.containsKey(element.getId())) {
						elementsGroupedIds.put(element.getId(), elementGroupedId);
				 }

				 if (!elementsGroupedElements.containsKey(elementGroupedId)) {
						elementsGroupedElements.put(elementGroupedId, element);
				 }
			}


			Map<Date, Map<Integer, HttpSessionStatistics>> statisticsPerPeriodMap = new HashMap<Date, Map<Integer, HttpSessionStatistics>>();

			long startTime = System.currentTimeMillis();

			List<StatisticsEntity> statisticsList = this.fileStatistics.search(elementsGroupedIds.keySet(), parameters.getDateFrom(), parameters.getDateTo());

			for (StatisticsEntity statisticEntity : statisticsList) {

				 //System.out.println("HTTP SESSION ENTITY: " + statisticEntity.getDate() + " --> " + statisticEntity.getSessionsDestroyedStatistics().getSum() + " + " + statisticEntity.getSessionsDestroyedTimeNanosStatistics().getSum());

				 //Get grouped id
				 int elementGroupedId = elementsGroupedIds.get(statisticEntity.getElementId());

				 //Get statistics sampling period
				 Date samplingDate = DatabaseUtilities.calculateStatisticTimestamp(statisticEntity.getDate(), parameters.getSamplingPeriod());

				 HttpSessionStatistics statistics = null;

				 Element element = elementsGroupedElements.get(elementGroupedId);

				 if (parameters.getSamplingPeriod() == RequestParameter.SAMPLING_PERIOD_NONE) {
						if (parameters.getGroupBy() == RequestParameter.GROUP_BY_CONTEXT_PROJECT) {
							 statistics = new TotalStatistics(statisticEntity, element.getProject(), this.samplingPeriod);
						}
				 } else {
						statistics = new TimeStatistics(samplingDate, statisticEntity);
				 }

				 if (!statisticsPerPeriodMap.containsKey(samplingDate)) {
						statisticsPerPeriodMap.put(samplingDate, new HashMap<Integer, HttpSessionStatistics>());
				 }

				 if (statisticsPerPeriodMap.get(samplingDate).containsKey(elementGroupedId)) {
						statisticsPerPeriodMap.get(samplingDate).get(elementGroupedId).add(statistics);
				 } else {
						statisticsPerPeriodMap.get(samplingDate).put(elementGroupedId, statistics);
				 }
			}

			long stopTime = System.currentTimeMillis();

			System.out.println("Time: " + (stopTime - startTime));

			final Collection<HttpSessionStatistics> resultList = new ArrayList<HttpSessionStatistics>();

			for (Date date : statisticsPerPeriodMap.keySet()) {
				 Map<Integer, HttpSessionStatistics> statisticsMap = statisticsPerPeriodMap.get(date);

				 for (HttpSessionStatistics statistics : statisticsMap.values()) {
						if (statistics instanceof HttpSessionStatistics.TotalStatistics) {
							 ((TotalStatistics) statistics).calculateTrendStatistics();
						}

						resultList.add(statistics);
				 }
			}

			return resultList;
	 }

	 synchronized void deleteStatistics(DeleteStatisticsParameter parameters) throws IOException {

			JeeObserverServerContext.logger.log(Level.FINE, "Deleting Http Session statistics. {0}", parameters.toString());

			//Filter elements
			Collection<HttpSessionStatistics.Element> elements = searchElements(new HttpSessionParameters.SearchElementsParameter(parameters.getElementId(), parameters.getContext(), parameters.getProject()));

			Set<Integer> elementsSet = new HashSet<Integer>();

			for (HttpSessionStatistics.Element element : elements) {
				 elementsSet.add(element.getId());
			}

			//Delete statistics
			this.fileStatistics.delete(elementsSet, parameters.getDateFrom(), parameters.getDateTo());


			Set<Integer> elementsToNotDeleteSet = this.fileStatistics.getActiveElementIds();

			//Delete elements
			this.fileElements.reset();

			ElementEntity elementEntity;

			while ((elementEntity = (ElementEntity) this.fileElements.read()) != null) {

				 if (elementEntity.getStatus() != DatabaseElementEntity.STATUS_DELETED) {
						if (!elementsToNotDeleteSet.contains(elementEntity.getElement().getId())) {
							 elementEntity.setStatus(DatabaseElementEntity.STATUS_DELETED);
							 this.fileElements.writePervious(elementEntity);
						}
				 }
			}
	 }

	 synchronized void close() throws IOException {
			this.fileStatistics.close();
			this.fileElements.close();
	 }

	 private static class ElementEntity extends DatabaseElementEntity {

			private static final long serialVersionUID = 40L;

			private Element element;

			ElementEntity(ByteBuffer buffer) {
				 super(buffer.get());

				 this.element = new Element(buffer.getInt(), DatabaseUtilities.getString(buffer, buffer.getInt()), DatabaseUtilities.getString(buffer, buffer.getInt()));
			}

			ElementEntity(byte[] bytes) {
				 this(ByteBuffer.wrap(bytes));
			}

			ElementEntity(String context, String path) {
				 super(DatabaseElementEntity.STATUS_ACTIVE);
				 this.element = new Element(context, path);
			}

			@Override
			public byte[] getBytes() {
				 ByteBuffer buffer = ByteBuffer.allocate(this.getSize());

				 buffer.put(this.getStatus());

				 buffer.putInt(this.element.getId());

				 buffer.putInt(this.element.getContext().getBytes().length);
				 buffer.put(this.element.getContext().getBytes());

				 buffer.putInt(this.element.getProject().getBytes().length);
				 buffer.put(this.element.getProject().getBytes());

				 return buffer.array();
			}

			@Override
			public int getSize() {
				 return (1 + 4 + 4 + 4 + element.getContext().getBytes().length + element.getProject().getBytes().length);
			}

			@Override
			public Element getElement() {
				 return element;
			}

			public boolean passFilter(Integer id, String context, String path) {

				 if (this.getStatus() == DatabaseElementEntity.STATUS_DELETED) {
						return false;
				 } else if (id != null && id.intValue() != this.element.getId()) {
						return false;
				 } else if (context != null && !context.equals(this.element.getContext())) {
						return false;
				 } else if (path != null && !path.equals(this.element.getProject())) {
						return false;
				 }

				 return true;
			}
	 }

	 private static class StatisticsEntity extends HttpSessionStatistics implements DatabaseStatisticsEntity {

			private static final long serialVersionUID = 40L;

			public static final int BYTES_SIZE = 73;

			private final int elementId;

			private transient byte status;

			StatisticsEntity(ByteBuffer buffer) {
				 this(buffer.get(), buffer.getInt(), new Date(buffer.getLong()), buffer.getInt(), buffer.slice());
			}

			StatisticsEntity(byte[] bytes) {
				 this(ByteBuffer.wrap(bytes));
			}

			StatisticsEntity(byte status, int elementId, Date date, int count, ByteBuffer buffer) {

				 super(date, count,
								 new DateStatistics(new Date(buffer.getLong()), new Date(buffer.getLong())),
								 FileDatabaseHandler.getNumberStatistics(buffer),
								 FileDatabaseHandler.getNumberStatistics(buffer),
								 FileDatabaseHandler.getNumberRangeStatistics(buffer));

				 this.elementId = elementId;

				 this.status = status;
			}

			StatisticsEntity(int elementId, Date date, HttpSessionObserver observer) {

				 super(date, 1,
								 new DateStatistics(observer.getDate()),
								 new NumberStatistics((observer.isClosed() ? Double.NaN : 1)),
								 new NumberStatistics((observer.isClosed() ? 1 : Double.NaN)),
								 new NumberRangeStatistics((observer.isClosed() ? observer.getTimeNanos() : Double.NaN)));

				 this.elementId = elementId;

				 this.status = DatabaseStatisticsEntity.STATUS_ACTIVE;
			}

			@Override
			public byte[] getBytes() {

				 ByteBuffer buffer = ByteBuffer.allocate(BYTES_SIZE);

				 buffer.put(this.status);
				 buffer.putInt(this.elementId);

				 FileDatabaseHandler.fillBuffer(buffer, this);

				 FileDatabaseHandler.fillBuffer(buffer, this.getDateStatistics());

				 FileDatabaseHandler.fillBuffer(buffer, this.getSessionsCreatedStatistics());

				 FileDatabaseHandler.fillBuffer(buffer, this.getSessionsDestroyedStatistics());
				 FileDatabaseHandler.fillBuffer(buffer, this.getSessionsDestroyedTimeNanosStatistics());

				 return buffer.array();
			}

			@Override
			public int getElementId() {
				 return elementId;
			}

			@Override
			public int getSize() {
				 return BYTES_SIZE;
			}

			@Override
			public byte getStatus() {
				 return status;
			}

			@Override
			public void setStatus(byte status) {
				 this.status = status;
			}

			@Override
			public void add(DatabaseStatisticsEntity entity) {
				 super.add((StatisticsEntity) entity);
			}
	 }
}
